查看原文
其他

58商家通Android端WebView加载优化方案

赵兵 58技术 2022-03-15


导语

本文从实际需求出发,通过分析Android端的Webview加载流程以及加载过程中可以优化的耗时点,分阶段优化加载速度,最终实现在一秒内加载H5页面,希望对有此需求的开发者有所启发和帮助。



背景

目前58商家通对58来说是一个连接B端平台,对于商家来说是一个运营管理工具,商家可以在58商家通上进行商业服务(精准、置顶)、信息沟通、帖子管理等基本运营操作而获取服务保障、访客足迹、会员权益等服务。由于58商家通是一个平台软件,随着规模的扩大,接入兄弟部门服务也越来越多,如推荐有奖,服务保障,放心服务,到家精选,福利商城等,而接入兄弟部门的服务都是通过H5的形式接入,因此,58商家通上的H5页面比例已经超过了Native 页面的比例,由于H5页面的加载效率远远低于Native的加载效率,所以对于58商家通的H5加载效率成为了重中之重的问题,优化这个问题,首先可提高用户体验和APP的活跃度、流量,其次接入各个服务之后能够提高用户的体验意愿,对于新服务在58商家通的推广也有很大的意义,故此将优化过程中遇到的问题及解决方案跟大家分享一下,希望能给大家一些帮助。


webview默认加载流程分析

1、webview默认加载流程

在优化webview加载H5页面之前我们需要了解默认webview加载H5页面的流程,并针对特定的耗时流程做出符合我们开发技术的方案。首先,webview首次加载流程大概分为以下几个阶段:





  A、webview的初始化

  B、浏览器内核初始化(全部webview共享,第一次初始化)

  C、请求html页面,并对页面进行解析
  D、下载解析过程中需要下载的js,css,图片等资源文件
  E、生成h5的domTree
  F、根据上文的domTree渲染页面

2、webview优化过程分析
分析之前,先解释下文中的一个名词(usdt服务:是58自主开发的一个管理web资源版本的scf服务,其主要功能是对资源文件通过添加版本后缀来实现版本管理。主要实现方式:上线资源时,给资源文件自动化加个时间戳后缀,主要实现如下:
String resultUrl = jsurl.replace(suffix, "_v" + version + suffix);//替换前端文件后缀,拼成html//举例用法:getVersion("https://test.58.com/test.js", ".js");//https://test.58.com/test_v2019555555555.js

然后我们看下之前说的流程中,其中A-D步骤都是页面白屏,本文中的测试页面是我们58商家通中相对资源比较多的页面,所以首次加载耗时相当严重,白屏超过3秒,二次加载也需要大概2秒多,所以这对于使用者是难以忍受的,优化过程大致分为以下几个阶段:
    A、Webview缓存的优化,使用自定义缓存替代webview自带缓存
webview默认加载过程中不可避免的需要使用到缓存,由于我们h5页面的开发行为以及使用到的一些技术,如果使用webview自带的缓存api去实现缓存逻辑,将会有以下一些问题:

  • API固定,依赖系统自带的API,如果需要扩展,如果系统不支持,很难二次开发。

  • 由于我们的h5页面图片都已经使用了cdn服务,而我们默认配置了8台服务器,所以相同的资源文件在客户端可能重复下载多次,造成流量和存储的浪费。

  • 现在大多数公司都会对资源文件做版本控制(为了解决资源文件内容修改之后,前端不能及时更新资源的问题),其中我们58就使用了相应的usdt服务,对于js,css文件使用具有usdt相同功能的服务,自带版本号,如果使用默认webview的缓存,新版本更新之后不能及时删除老版本的js,css文件问题。

  • 默认缓存,对于缓存策略和文件的操作都不可扩展,对于我们来说是个黑盒子。

由于以上等原因我们做了第一阶段的优化,使用自定义缓存替代webview自带的缓存。
  B、预先初始化Webview,可提前初始化浏览器内核,并预加载页面,在父页面提前根据策略加载需要加载的页面。
APP启动之后就定义一个全局的webview对象实例,因为在我们第一次使用webview初始化过程中,需要初始化浏览器内核大概500ms左右,可以对此进行优化,其次,在我们第一次进入某页面之后,在页面初始化View组件的同时,使用默认的webview将资源文件缓存到本地,等到View组件初始好之后,可以直接使用本地缓存的资源进行渲染,以提高页面加载速度。并且使用该全局webview对象在父页面提前缓存子页面,这样在加载子页面的时候提前从本地缓存加载页面。减少第一次下载页面资源以及解析生成中间数据的时间。
  C、特定的场景使用H5离线包,首次请求进行下载,二次进入场景如果需要显示直接从本地显示。
特定的场景:例如,显示推广活动的H5页面,定期宣传的服务H5页面等等,
服务器会提供相关接口告知APP,APP在首次启动的时候下载资源,当下载完成之后,App第二次进入时候,直接从本地加载。
优化之后的整体架构如下:





下一章节将具体说明优化方案的全过程。


Webview加载优化方案实践过程

1、默认加载速度测试过程

由于第一章节说了使用webview自带缓存的问题,所以默认测试速度的时候是禁用系统的缓存的,并且加载了的测试页面(58商家通中的商学院页面),关键代码如下:

webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);webView.loadUrl("https://hyapp.58.com/app/school/open/articles/tohome");

然后测试10次,并记录每次各项数据如下:





分析:
创建页面时间:onCreate()开始时间
页面加载时间:onPageStarted()开始时间
页面加载完成时间:onPageFinished()开始时间
初始化耗时:从onCreate()到onPageStarted的时间
加载资源耗时:从onPageStarted()到onPageFinished()的时间
总耗时:从onCreate()到onPageFinished()的时间
由数据可以看出:

  • 第一次资源缓存时间大概3.1s左右,第二次资源缓存大概2s左右

  • 第一次总耗时大概3.9s,第二次大概2.3s左右

  • 第一次初始化耗时大概800ms,第二次初始化耗时300ms

所以为了缩短总耗时,首先需要优化缓存这个最耗时的步骤,下面我们将说明缓存优化的过程。


2、缓存优化的过程

首页我们来开看缓存优化的切入点以及具体的流程如下图所示:





   A、切入点:当webview 需要加载资源的时候,会使用下面两个api进行拦截。

/** * 发生资源加载,拦截顺序 * * 此方法添加于API21,调用于非UI线程,拦截资源请求并返回数据,返回null时WebView将继续加载资源 * @param view * @param request * @return */ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)

/** * 此方法废弃于API21,调用于非UI线程拦截资源请求并返回响应数据,返回null时WebView将继续加载资源 * @param view * @param url * @return */ public WebResourceResponse shouldInterceptRequest(WebView view, String url)

  B、当拦截到需要下载网页资源的url后,我们需要以下几点需要明确:

  • 哪些url文件需要缓存?

  • 对于js,css文件等如何更新?

  • 多台cdn服务图片如何只下载一份?

解决这些问题之前:首先我们内部开发约定如下:

  • js,css文件上线需要继承usdt等相近的服务。

  • 图片资源应上传至cdn服务器,从cdn服务器应用。

之后,开始解决上面的问题,首先我们app端只缓存了符合我们规定的资源文件
大概占95%以上,对于不符合规定的资源文件依旧是从网络获取,来保证我们的页面的正确性。所以我们只针对具有版本号的js,css文件,已经cdn服务器的图片进行缓存。其次来看看文件的更新策略,由于我们需要缓存的js,css文件都是携带版本号的(https://j1.58cdn.com.cn/shangjiatong/sdk/sj_app_v20190327110116.js)我们会以携带的版本号来判断文件是否需要更新,如果需要更新,则直接异步缓存文件,并删除之前的旧版本,并同时让webview从网略加载需要更新的资源。最后对于多台cdn服务器缓存的同一个图片资源的url是不同的例如:

https://pic1.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpghttps://pic2.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpghttps://pic3.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg

所以可以根据这个特点,做细节处理,对于后缀相同的图片值缓存一次,避免重复下载。后期服务端会做路由和分发,可以直接避免此问题。我们在优化之后使用相同的手机和相同的页面进行测试,测试结果如下:





由测试结果可以看出,第一次加载资源耗时因为是从网络缓存文件1709ms,第二次由于直接从缓存获取已经降低到了416ms。而页面初始化的时间第一次依旧需要800ms左右,第二次需要300ms左右,但可以看出如果第二次初始化都是300ms,比第一次少了500ms左右,这因为第二次加载页面首先不需要初始化浏览器内核,第二是第一次加载页面之后,会对一些临时、简单数据进行缓存,Cookies的扩展。具体的API如下

webView.getSettings().setDomStorageEnabled(true);

正因为第二次比第一次加载明显变快,所以能不能将第一次加载也做成是第二次加载呢?因此带着这个问题我们进入了下一个流程的优化。


3、初始化全局webview阶段,并提前预加载页面

我们在APP使用Webview加载一个页面总感觉比在手机浏览器中打开同一个页面会慢,这主要是因为当我们在手机浏览器中打开页面之前,我们已经打开了手机浏览器这个APP,打开完成之后,它已经对浏览器内核进行了初始化。而当我们打开自己的APP去加载页面时候,当加载页面的时候才会去初始化webview,然后第一个初始化webview 就会初始化浏览器内核,所以会比浏览器慢,为了解决这个问题,我们可以如下优化。
在App启动之后,定义了一个全局的WebviewProxy单例对象,它会持有一个webview对象,首先,在初始化它的时候,会初始化浏览器内核,下次进入页面初始化webview时会更快,由之前的数据可以看出大概会提高500ms;其次,通过这个webview对象可以在父页面提前缓存子页面,这样加载子页面时候可以快速显示子页面。
针对上面的方案,实践过程中需要注意以下几个问题?

  • 初始化webview持有上下文环境?

  • 当父页面加载子页面还没有完成时,点击子页面如何处理?

  • 父页面为H5页面,子页面很多,如何动态配置加载子页面?

  • 启动APP的时候如何将本地文件加载到内存?

首先,webview持有的上下文环境如果直接传当前页面的上下文环境,如果当前需要退出,由于被全局webview对象持有,所以会导致内存泄漏,如果使用MutableContextWrapper类去持有当前页面Context,需要在销毁页面时候去主动调用setBaseContext()方法去释放当前Context,由于我们的webview本来就是全局唯一的单例对象,所以我们为其分配了Applciation对象作为Context对象。
具体的定义全部的webview的代理对象:

public class WebviewProxy implements IWebviewProxy{ private WebView webView; private static volatile WebviewProxy INSTANCE; private WebviewProxy(){ webView = new WebView(MyApplication.getInstance()); initWebView(); } public static WebviewProxy getInstatnce(){ if(INSTANCE == null){ synchronized (WebviewProxy.class){ if(INSTANCE == null){ INSTANCE = new WebviewProxy(); } } } return INSTANCE; } @Override public void load(String url) { webView.loadUrl(url); } ...}

其中在需要提前加载页面时可以通过load方法,提前缓存页面以及生成domTree,如下所示。

private void initWebview() { WebviewProxy.getInstatnce().load("https://hyapp.58.com/app/school/open/articles/tohome");}

第二个问题当父页面加载子页面还没有完成时,点击子页面时如何处理,这里主要关注的点是缓存资源可能存在重复下载的问题,所以在做此处的时候需要对上面的下载组件进行了重构,加入了任务队列模块,所以,在点击子页面的时候,如果已经缓存了则直接从缓存获取,如果没有,则判断是否在缓存队列,如果在,则不需要重新缓存,如果不在,才会下载,这样就避免了同一资源重复下载的问题。
第三个问题父页面为H5页面,如何动态加载子页面,对于这个问题,我们对H5页面提供了jsbridge协议,当父页面需要缓存时,直接调用Native提供的协议即可,这样H5开发过程中,会自己根据判断是否需要加载子页面,而动态的调用协议去缓存。
第四个问题,启动的时候如何将本地文件加载到内存中,如果将本地的缓存文件全部加载到内存中,如果缓存文件过多,太消耗内存,所以我们做了动态配置,以及优先级等策略,首先,文件保存到本地会设置文件优先级,核心页面为1-10,普通一级页面为10-100,二级页面为100-1000,其次,配置加载的内存大小,所以,首次启动APP之后,我们按优先级最高的一个一个文件加载到内存中,并判断是否到达最大内存限制,对需要初始化的资源进行管控。
最后还是使用我们商学院的的页面做测试,我们再首页启动的时候就预先使用全局的WebviewProxy这个对象去加载商学院url,然后,点击商学院页面,进入商学院页面,对其进行了测试,数据结果如下:





可以看出初始化全局webview并且预加载页面之后,我们的58商家通中资源消耗最多的商学院页面加载速度也达到了1秒以内,并且第二次和第一次加载耗时基本相近。


4、特定场景下,支持离线包

之前的流程在一般场景下都是可以适用的,最后针对我们特定的业务需求,又做了离线包加载模块,首先,来看下场景:我们需要动态的在APP启动的时候显示可配置的H5广告,因为是APP启动,所以应该以最快速度显示页面,所以需要将所有的H5页面以及资源打包,当我们启动的时候,首次先请求接口,如果有需要展示的广告,先下载本地并解压,之后启动APP的时候判断如果还需要显示,就直接从本地加载,这样可以以最快速度加载我们的H5页面,而且是否显示,显示的广告内容,都是服务器可配置的,方便产品做运营推广,这一节其实只是一个场景的补充,与上面几部的优化没有什么关联,但是提供了另一种优化方案(APP提供浏览器的壳,所有的资源可动态加载到本地,之后直接加载本地页面和资源)。具体流程如下图所示:






持续优化计划

上文优化过程中还有许多需要后期优化的点,后期持续优化计划如下:
首先会对缓存文件的初始化逻辑优化,缓存策略的优化,减少运行过程中对File的操作,能直接命中内存中的缓存。其次是对架构中各模块的封装,降低各模块之间的耦合性,最后,希望能够封装成sdk,直接在其他项目中集成使用。


总结

技术服务于业务需求,由于58商家通中的H5页面比例的增多,所以对于我们58商家通平台来说优化H5页面的加载速度显得格外重要,所以经过一系列的实践和方案的实施,使得我们58商家通APP的H5页面加载到达了秒级显示,因此性能优化在实践中得到了验证,所以出此文章供大家参考。我也一直会继续努力优化我们的58商家通这款App,后期计划的优化点还有许多,使得商家能够简单高效的在我们58上做生意,提高我们产品的体验度,对此如果大家还有别的方案,也希望能够多多交流,以后,争取能够发表更多关于前端Android技术的分享。温故而知新,希望这次总结,也能对自己有所帮助。


作者简介

赵兵,58商家通全栈开发工程师,主要负责58商家通前后端业务开发、性能优化、架构设计、重点项目和版本迭代,长期参与58商家通需求开发迭代,并偶尔参与本部门其他服务端开发如推荐有奖、广告平台、企管家等服务。


END

阅读推荐

并发在58二手车列表的应用
58App-Android端的动态化框架实践与思考
API管理平台之SCF服务测试篇
API管理平台之系统设计篇
开源|Magpie可视化圈选埋点实践
开源|Magpie:组件库详解







您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存